﻿using System;
using System.Collections.Generic;
using Heaven.Geometry;
using Heaven.Graphics;
using System.IO;
using System.Reflection;

using Direct3D9 = SlimDX.Direct3D9;
using Slimdx = SlimDX;

namespace Heaven.SlimDX.Graphics
{
    /// <summary>
    /// Caches device representaions of shader objects
    /// </summary>
    class ShaderManager
    {
        #region Fields

        // Graphics device
        Direct3D9.Device device = null;
        // Cache entity
        Dictionary<long, Direct3D9.Effect> cache = new Dictionary<long, Direct3D9.Effect>();

        // One HLSL effect can be shared between several techniques, 
        // so we cannot dispose effect before at least one technique with this HLSL code existed
        Dictionary<long, int> usingCounter = new Dictionary<long, int>();

        // For fast access to the object
        long lastUsedHash = 0;
        Direct3D9.Effect lastUsedEffect = null;
        
        // Effect pool
        Direct3D9.EffectPool pool = new Direct3D9.EffectPool();
        Direct3D9.Macro[] preprocessorDefines = new Direct3D9.Macro[0];
        Direct3D9.Include includeFile = null; 


        #endregion

        #region Properties

        /// <summary>
        /// Count of shaders
        /// </summary>
        public int Count
        {
            get { return cache.Count; }
        }

        /// <summary>
        /// Shader - representation for objects with errors
        /// </summary>
        private string ErrorShaderSource
        {
            get
            {
                using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(
                       "Heaven.SlimDX.Resources.Shaders.Error.fx"))
                {
                    StreamReader reader = new StreamReader(stream);
                    return reader.ReadToEnd();
                }
            }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="device">Graphics device</param>
        public ShaderManager(Direct3D9.Device device) 
        {
            this.device = device;
        }

        /// <summary>
        /// Destructor
        /// </summary>
        ~ShaderManager() { Dispose(); }

        /// <summary>
        /// Disposes unmanaged resources
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            foreach (Direct3D9.Effect effect in cache.Values)
            {
                effect.Dispose();
            }
            if (pool != null)
            {
                pool.Dispose();
                pool = null;
            }
            cache.Clear();
        }


        #endregion

        #region Methods

        /// <summary>
        /// Gets a shader object
        /// </summary>
        /// <param name="shader">Shader object</param>
        public Direct3D9.Effect Get(Shader shader)
        {
            // A little optimazation
            if (shader.Hash == lastUsedHash) return lastUsedEffect;

            Direct3D9.Effect effect = null;
            lock (cache)
            {
                if (!cache.TryGetValue(shader.Hash, out effect))
                {
                    // Create shader
                    string compilationErrors = "";
                    try
                    {
                        effect = Direct3D9.Effect.FromString(device, shader.Source, preprocessorDefines, includeFile, null,
                            Direct3D9.ShaderFlags.OptimizationLevel3 | Direct3D9.ShaderFlags.EnableBackwardsCompatibility, pool, out compilationErrors);
                    }
                    catch (Exception exception)
                    {
                        System.Diagnostics.Debug.WriteLine("Errors (" + exception.Message + ") during compiling shader. " + compilationErrors);
                        // Try to use error shader
                        effect = Direct3D9.Effect.FromString(device, ErrorShaderSource,
                            Direct3D9.ShaderFlags.None);
                    }
                    // Put into cache
                    cache.Add(shader.Hash, effect);
                    // And when object will be collected by GC
                    shader.GarbageCollected += new EventHandler(OnShaderGarbageCollected);
                }

                lastUsedHash = shader.Hash;
                lastUsedEffect = effect;
            }

            return effect;
        }

        void OnShaderGarbageCollected(object sender, EventArgs e)
        {
            lock (cache)
            {
                long hash = ((Shader)sender).Hash;
                Direct3D9.Effect effect = null;
                if (cache.TryGetValue(hash, out effect))
                {
                    // Remove the effect from 
                    // cache, so it is not valid now
                    cache.Remove(hash);
                    usingCounter.Remove(hash);
                    effect.Dispose();
                    if (lastUsedHash == hash)
                    {
                        lastUsedHash = 0;
                        lastUsedEffect = null;
                    }
                }
            }
        }
        
        #endregion
    }
}
